feat(questionnaires): F8.3 anonymous-mode hardening + respondent profile#55
Merged
Merged
Conversation
Make anonymousMode a real PII contract honoured at every data boundary, and add the respondent profile-collection capability the contract gates. - AppRespondentProfileSnapshot model (+ migration); both User and session FKs onDelete: Cascade so eraseUser() removes snapshots natively - Capture seam in createSessionFromInvitation: validates + writes snapshot only on non-anonymous invitation path; anonymous/version-direct/no-login never capture (D1 — no row, not an empty row) - Respondent profile-start form, gated onto the start page via loadStartContext - Read-path redaction across exports (results loader/serialize, session-export, PDF document, build-session-export-model): profile nulled when anonymous - Analytics hardening: k-anonymity (K=5) cohort suppression in cost/funnel/ distributions; cost drops per-session table when anonymous; admin panels render suppressed states - Privacy constants module (analytics/privacy.ts), docs (anonymous-mode.md), tracker (features/f8.3.md) No CHANGELOG entry — app-owned models/routes are outside the Sunrise platform surface.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Turns
anonymousModefrom a loose "open / no-invitation" flag into a real PII contract honoured at every data boundary — and adds the previously-unbuilt respondent profile collection capability so there's a snapshot for that contract to gate. Gated byAPP_QUESTIONNAIRES_ENABLED.The canonical contract (invariant, per-surface gate table, snapshot rule, k-anonymity, erasure cascade) lives in
.context/app/questionnaire/anonymous-mode.md.What changed
Model + erasure
AppRespondentProfileSnapshotmodel (migration20260609062611_app_respondent_profile_snapshot).UserFK and the session FK areonDelete: Cascade—eraseUser()removes snapshots via the native cascade, no cleanup hook. Schema test asserts both FKs cascade (the GDPR contract).Capture seam (D1 — no row, not an empty row)
createSessionFromInvitationvalidates and writes the snapshot only on the non-anonymous invitation path with profile values present.anonymousMode = truemeans no PII is collected at all. A test assertscreateis never called for anonymous.Respondent form
profile-start-form.tsx(react-hook-form + Zod +FieldHelp), gated onto the start page vialoadStartContext— renders only for a non-anonymous version that declares profile fields.Read-path redaction (defence in depth)
respondent_profilecolumn),session-export.ts,build-session-export-model.ts, and the session PDF document.Analytics hardening
K_ANONYMITY_THRESHOLD = 5cohort suppression in cost / funnel / distributions; aggregate spend always returned (no identity).suppressed/topSessionsSuppressedresult fields + a{ kind: 'suppressed' }distribution variant; admin panels render the suppressed states.Tests
profile-snapshot.test.ts— capture invariants (anonymous never writes; invalid/empty rejected/skipped; no capture on resume).profile-values.test.ts— strict validator.analytics/{cost,funnel,distributions}.test.ts— suppression + anonymous guard.session-export.test.ts— profile surfaced when not anonymous, dropped when anonymous.app-questionnaire-schema.test.ts— model shape + both FKsON DELETE CASCADE.npm run validatepasses (type-check + lint + format).CHANGELOG
None — app-owned models/routes are outside the Sunrise platform surface, per the repo's platform-scoped CHANGELOG policy.
🤖 Generated with Claude Code